Delphi App Translation Studio

Engineering Guide

How the product is built, why it is built that way,
and what holds it together

Last changed: August 21, 2026
Applies to: alpha, build 2026.08.22.129
Frameworks: Delphi VCL and FireMonkey  ·  Platform: Windows (Studio)

Written from the source. Every number, file name, and unit reference in this
document was taken from the code as it stands on the date above.

Contents

Headings use the built-in Heading 1–3 styles, so a live page-numbered table of contents can be inserted over this list at any time through References > Table of Contents.

  1. Purpose and audience
  2. What the product does
  3. Design principles
  4. Architecture
  5. Core types and the workspace
  6. The scan
  7. Context: telling the service what a string means
  8. The catalog and its validator
  9. Translation providers
  10. Terminology, dictionaries, and hyphenation
  11. The layout planner
  12. Right-to-left layout
  13. The runtime pack
  14. The runtime
  15. Contracts: how this product is kept honest
  16. Extending the product
  17. Build and release validation
  18. Known limits

Appendix A — Unit inventory
Appendix B — Contract inventory and framework parity
Appendix C — Files, folders, and where things live

1. Purpose and audience

This guide is for the engineer who has to change this product: to fix a defect, add a capability, or judge whether a proposed change is safe. It assumes fluency in Delphi and no prior knowledge of this codebase.

It is not a user guide. It does not explain how to run the Setup Wizard; it explains what the Wizard is doing and why.

It is organized to be read straight through once and then used as a reference. Sections 4 through 14 follow the path a string takes through the product, in order: scanned out of a form, described, translated, measured, planned, exported, and applied. Sections 15 and 16 describe how the work is verified and how to extend it.

Where a design decision was reached by measurement rather than by reasoning, the measurement is given. Where something is known to be incomplete, it is said so plainly and pointed at the Fix List rather than glossed over.

2. What the product does

Delphi App Translation Studio takes a Delphi application and produces the files needed to run that application in another language, including the layout adjustments the new language requires, because translated text is rarely the same size as the original.

The product performs six jobs in sequence:

The deployed application needs no API key and no internet connection. Everything that requires a network happens once, on the developer's machine.

3. Design principles

Almost every design decision in this codebase follows from one of five commitments. When a change seems to conflict with the surrounding code, it is usually because it conflicts with one of these.

3.1 The target application is evidence, never a workpiece

The product reads the target project's .dfm, .fmx, .pas, .dpr, and .dproj files and never writes to them. Every artifact it produces lives outside the target tree. This is not a configuration option; it is the reason a developer can point the tool at a working application without taking a risk.

It has a consequence that recurs throughout this guide. Where a translation genuinely requires a change to the application, such as a TranslateText call for a string the application composes itself, the product's job is to say so precisely, not to make the change.

3.2 The runtime is offline

Translation happens once, at development time. What ships is a JSON pack read from disk. No key, no network, no telemetry. This separates the one phase that needs the internet from the one that must never depend on it, and it means a translated application behaves identically on a machine with no network at all.

3.3 Artifacts are readable and diffable

The catalog, the layout proposal, and the runtime pack are JSON. They can be read in a text editor, compared between versions, committed to source control, and corrected by hand. Several defects described in this guide were diagnosed by reading those files rather than by debugging, which is an argument for the format on its own.

3.4 One decision lives in one place

The list of layout properties a pack may carry once existed in four units. They drifted, and each drift silently deleted a feature: the planner decided correctly, the runtime would have applied correctly, and the pack in between simply did not carry the value. There was no error anywhere, and nothing failed.

That list now lives only in DAT.Runtime.LanguagePack, which the exporter and both applicators already reference. Section 15 describes the tests that guard the joins where copies like that tend to reappear.

3.5 Both frameworks, one implementation

VCL and FireMonkey differ in defaults, in property names, and in what the framework does on your behalf. The response is not two code paths but one implementation behind a seam. The clearest example is text measurement (section 11.1). The clearest payoff is right-to-left mirroring (section 12), where the VCL has native support, FireMonkey has none at all, and both are served by the same planner pass.

4. Architecture

4.1 The pipeline

 

 

Each stage writes a durable artifact and the next stage reads it. That is what makes the pipeline diagnosable: when something is wrong on screen, the question is always which artifact first contains the mistake, and every one of them can be opened and read.

The stages are deliberately not fused. It is possible to scan without translating, to translate without planning layout, to re-plan layout without re-translating, and to rebuild a pack from an edited catalog. Each of those is a real workflow, and each is possible only because the intermediate artifacts are real files.

4.2 Source layout

 

FolderUnitsLinesResponsibility
source\core103,497Catalog types, JSON, workspace paths, glossary, hyphenation, pack builder
source\scan103,741Reading forms and Pascal; context and domain profiling
source\review54,720The layout planner, the text measurement seam, code-owned geometry
source\provider81,371Machine translation: placeholders, batching, language codes, retry, credentials
source\runtime54,452What ships inside the customer's application
source\components72,064The language manager component and the language selectors
source\studio45,204The Studio and the Setup Wizard
source\integration52,172Component kits, packages, deployment
source\validation1403The catalog validator

 

Fifty-seven units, about 27,700 lines.

4.3 The runtime boundary

The division that matters most is runtime versus everything else. Units under source\runtime and source\components are compiled into the customer's application. Everything else runs only in the Studio and never ships.

The runtime units are held to stricter rules than the rest of the codebase. They must be small, must not reach for the network, must not depend on any Studio unit, and must never write to a user's disk uninvited. A dependency accidentally added from a runtime unit to a Studio unit would pull the entire translation pipeline into every customer application, so the direction of that dependency is worth checking whenever a runtime unit gains a new uses clause entry.

5. Core types and the workspace

5.1 The catalog data model

DAT.Core.Types holds the types every other layer passes around. The central one is the translation entry, and knowing its fields explains most of what the rest of the product does with it.

 

FieldHoldsUsed by
keyForm, component, and property path, such as frmMain.btnSave.CaptionThe runtime, to find the control again
sourceTextThe original textTranslation; returning to the source language
translatedTextThe translationEverything downstream
componentClassNameThe control's classContext; the planner's per-class rules
sourceFileName, sourceLineWhere the string was foundReview; reporting strings the application must handle itself
textOwnershipWho controls the string at run time (section 6.5)Deciding whether the pack can apply it at all
contextKind and context sentenceWhat the string means in this applicationThe translation request
sourceChecksumA fingerprint of the source textDetecting that a form changed under a saved decision
statusUntranslated, machine translated, reviewed, approvedReview; validation

 

The catalog itself adds the application identity, the framework, the source language, and the target locale, including the date, time, and number formats that language expects. The locale block matters more than it looks: applying a language is not only a matter of words, and the runtime sets format settings from it.

5.2 The workspace

DAT.Core.TranslationWorkspace owns every path the product writes to. No other unit composes a path by hand, which is what makes it possible to state in one place that nothing is ever written inside the target project.

Each project gets a workspace folder holding a development catalog per language, the built packs, and the saved layout decisions. Appendix C lists the locations.

5.3 Project detection

DAT.Core.ProjectDetection reads a .dproj to establish two things: which units and forms belong to the project, and which framework it targets. The framework answer decides which text measurement engine the planner will use, which property names appear in the pack, and which applicator the customer's application will link, so it is the first fact everything else depends on.

6. The scan

DAT.Scan.Project drives the scan. DAT.Scan.FormText reads designer files, DAT.Scan.PascalResources reads Pascal, and DAT.Scan.Rules classifies what they find.

6.1 What is collected, and what is deliberately not

The scan is limited to project-referenced source units and designer resources. Broad harvesting of Items.Add, Lines.Add, TextOut, and similar calls is disabled on purpose: in practice those carry data rows, file names, log lines, and generated content rather than stable interface text. An earlier version harvested them and produced thousands of strings that no user would ever see, which buries the few hundred that matter.

Designer-authored Items and Lines are still collected, because those come from the form file, where a developer put them deliberately.

6.2 Reading designer files

One parser reads both .dfm and .fmx. They are the same format with different property names, so the parser tracks object nesting and property assignment generically, and the framework differences are handled by name mapping: Caption against Text, Left against Position.X, Width against Size.Width, and so on.

Fonts are the awkward case. A .dfm records a font as Font.Height in negative pixels rather than Font.Size in points, and the designer writes Font.Size only if somebody typed it. Reading only Font.Size therefore leaves every VCL control at the default, and the planner sizes boxes for text nobody will see. The conversion is Round(-Height * 72 / 96), rounded to match what TFont itself does, and fonts inherit down the object tree from the form.

6.3 Collections and nesting

Grid columns are worth understanding before changing the parser. A VCL grid keeps its heading on the column's Title, so the catalog key reaches one level further in than the column itself:

grdSchedule.Columns[0].Title.Caption

Collections nest inside objects, and a collection item's end must not pop the object stack. Getting that wrong makes every control after the grid appear to be a form, which corrupts the rest of the file silently. A contract exists purely to hold that behavior in place (Appendix B).

FireMonkey has no equivalent hazard, because a FireMonkey column is an ordinary named component with a Header property rather than a collection item. The same capability is reached by a completely different path on each framework, which is a pattern worth expecting throughout the runtime.

6.4 Reading Pascal

The Pascal reader collects resource strings and string constants assigned to recognized interface properties. It records the file and line for everything it finds, which is what later allows the product to tell a developer exactly where a string it cannot handle actually lives.

6.5 Text ownership

Every string is classified by who controls it at run time. The classification decides what happens to the string for the rest of the pipeline.

 

ClassificationMeaningApplied automatically?
designerAutomaticSet in the form file and not touched by codeYes
runtimeWiredAssigned in code, but through a path the runtime can reachYes
runtimeUnwiredAssigned in code, composed at run timeNo — needs a call in the application
applicationDataA data value, not interface textNo
suspiciousLooks like data, or like something that should not be translatedNo

 

The third row is the one that surprises people. Where an application builds a caption in code and reassigns it whenever the display refreshes, anything the pack writes there is overwritten moments later:

StatusBar1.Panels[1].Text := 'Items in list: ' + ItemCount.ToString;

The classification is correct, and the translation is genuinely impossible without a TranslateText call in the application, which principle 3.1 forbids the product from adding. What the product does not yet do is report those strings to the developer, even though it holds the file and line for each one. That gap is on the Fix List.

6.6 Encoding

DAT.Scan.TextCodec handles the fact that a Delphi source file without a byte order mark is read in the machine's ANSI codepage. This is not a theoretical concern: non-ASCII characters in a source file that lacks a BOM arrive wrong, and the failure is silent. Section 15.5 describes the build guard that enforces the rule across this product's own source.

7. Context: telling the service what a string means

A machine translation service sees one string at a time. A short interface string carries almost no information on its own, and the service has to guess: a word that names a thing in one program is a verb in another, and a term that means one thing in a media player means something else entirely in a disk utility.

The product's answer is that everything needed to settle those questions is already in the scan. Nobody has to type it.

7.1 The application domain profile

DAT.Scan.DomainProfile reads the application rather than trying to recognize it. An earlier version matched a handful of subjects from keyword lists. It read some applications correctly and would have failed file utilities, database tools, point of sale, laboratory systems, and most of the long tail Delphi is actually used for. A recognizer can only recognize what somebody already thought of.

Two things come out of reading an application:

7.2 Word sense

Volume is loudness in an application that also says mute and speaker, and a disk in one that says partition and format. Mask is a filename pattern where the application talks about filenames. Nothing in the code has to know what kind of application it is looking at; the vocabulary decides.

Where the application settles nothing, nothing is said. A guess between two senses is worse than silence, because it reaches the service as a confident instruction to be wrong.

7.3 Part of speech

A button says what pressing it will do, so its caption is an instruction. A menu item names a thing. English hides the difference, because for most verbs the imperative and the dictionary form are the same word. Many other languages do not, and a service given a bare word with no indication of which is wanted may return a grammatically correct statement where an imperative was needed.

DAT.Scan.Context therefore states which is wanted, chosen from the control class that every string already carries: an imperative for a button, the form the language uses on menus for a menu item, a noun phrase for a column heading or a label.

7.4 Delivering context to the string it describes

A service accepts one context per request. An implementation that batches fifty strings into one request therefore has one context field for fifty different descriptions, and concatenating them means every string arrives wearing forty-nine descriptions of other controls. The context is present, paid for, and diluted into uselessness.

The economics make the fix free. Billing is per translated character, not per request, and context characters are not billed at all. A mid-sized application costs the same number of billed characters whether it goes as six requests or as three hundred.

DAT.Provider.Batching therefore groups strings by identical context. Where a context is unique the group holds one string; where many strings share a context, or have none, they travel together as before. The only cost is round trips, paid once per language for a result that is then stored permanently.

8. The catalog and its validator

DAT.Core.CatalogJson reads and writes the catalog, which is the editable record of one application in one language. Each entry carries the fields listed in section 5.1. The catalog is meant to be edited: a reviewer can correct a translation in the Studio or in a text editor, and the correction survives a re-scan because entries are matched by key.

DAT.Validation.Catalog refuses to let a pack be built from a catalog with errors. This is not decoration. The most valuable class of error it catches is damage to format specifiers: a translation service may reorder, alter, or destroy a %s or %.2f, and a format string that reaches production damaged will fault or print nonsense at users indefinitely. The validator is frequently the only thing standing between a plausible-looking translation and a broken application.

Validation is a gate, not a warning. A catalog with blocking errors cannot be exported.

9. Translation providers

Two services are supported, both using the developer's own API key: DeepL and Google Cloud Translation. The key is held in Windows Credential Manager, or kept for the session only, and is never written to the catalog, the pack, or any log.

DAT.Provider.Client is the transport. Four units sit around it, and each one exists because of a distinct failure mode that a naive client walks straight into. Each is worth understanding before changing that code.

9.1 Placeholders

DAT.Provider.Placeholders lifts format specifiers out of a string before the request and replaces each with a token that carries its own index, so a token the engine moves still comes back identifiable as the specifier it stood for. Engines do move them; right-to-left target languages move them routinely.

Identical specifiers get separate tokens. A string containing three occurrences of %.2f cannot be restored by search and replace, because there is no way to tell which returned token was which.

Afterward the specifiers are restored and checked. If they do not match, the source text is returned rather than the translation. An untranslated string among translated ones is obvious in review; a damaged format string is invisible until a customer sees it.

A string that is nothing but specifiers is never sent at all. %.2d/%.2d is a date format, not a sentence, and there is nothing in it to translate.

9.2 Language codes

DAT.Provider.LanguageCodes converts a catalog's language code into one the service will accept. A catalog names languages the way Windows does, with a language and a region. Services accept a two-letter code plus a published list of regional variants, and reject anything else with a hard error.

A normalizer that passes the region through unchanged works for whichever languages happen to be on that list and can never work for the rest. The region is now kept only where the service is documented to accept it and dropped everywhere else. Dropping is the safe direction: the general code works for every language the service supports, so a language added after this was written still translates rather than failing.

9.3 Retry and pacing

DAT.Provider.Retry treats a rate-limit response as what it is: the service asking for a slower pace. The only wrong answer is to stop. Six attempts, waiting one second, two, four, eight, and sixteen — thirty-one seconds of patience. A Retry-After header is obeyed to the second where the service sends one, capped so that a large value cannot stall a run, and treated as absent rather than guessed at when it arrives in a form the client cannot parse.

Errors that will not improve with time are not retried. A malformed request, a refused key, and an exhausted quota all come back just as fast the second time.

The client also paces itself. The delay between requests starts at zero, grows by a quarter second each time a rate limit is met, and holds for the rest of the run. A run that is never refused pays nothing; a run that is refused once slows down instead of arguing with the service.

9.4 Reporting what the service said

Both services return an explanation in the response body, and an early version of the client discarded it in favor of a generic message listing six things that might be wrong. That is worse than useless: it tells a developer to check everything. Rejections now quote the service's own explanation and add one line where the status code carries meaning of its own.

10. Terminology, dictionaries, and hyphenation

10.1 The three shared stores

Three shared, editable stores live outside any one project, so that work done on one application benefits the next. All three are plain JSON and are meant to be corrected by hand.

 

StoreScopePurpose
Dictionaries\<lang>.jsonPer languageApproved wording earned on one application, available to every later one
Terms\ambiguous-terms.jsonOne file, EnglishWords ambiguous in a user interface, with the evidence that settles each sense
Hyphenation\<lang>.jsonPer languageWhere a long word may be broken

 

A per-project glossary (DAT.Core.Glossary) sits above these for terms specific to one application, and DAT.Core.Terminology resolves the two against each other when a request is built.

10.2 Hyphenation

Some languages build a single long word where English uses three, and a single word cannot wrap: there is no space in it for a control to break at, so it is simply cut off at the edge of its box. No amount of widening fixes the general case, because the next word may be longer still.

DAT.Core.Hyphenation holds a per-language dictionary describing which letters are vowels, which consonant groups form a single sound, and how much of a word must be left whole at each end. From that it marks every point where the language allows a break.

The marks are carried in the pack as soft hyphens (U+00AD), because nothing at build time knows how wide a control will end up. They are applied to captions only. Format strings are left exactly as written, since their text goes on to be filled with data and a mark in the middle of a specifier would corrupt it.

10.3 How the two frameworks differ on soft hyphens

This is the clearest example in the product of two frameworks requiring opposite treatment, and it was measured rather than assumed.

 

 FireMonkey (DirectWrite)VCL (GDI)
A mark that is not usedInvisible. A marked word measures exactly what the unmarked word measures.Drawn as an ordinary hyphen. The word measures wider for every mark in it.
A mark that is neededUsed as a break opportunity. The word breaks at a syllable.Ignored. DrawText will not break a line at U+00AD.
What the runtime doesNothing. The marks reach the caption and the renderer chooses.Resolves every mark before it reaches a caption.

 

So DAT.Runtime.VCL resolves the marks itself, and does it at the last possible moment: after every layout rule has been applied and each control is the size it will really be. A control that wraps gets a real hyphen and a real line break at the last mark that fits its width; one that cannot wrap gets the plain word with the marks removed. No soft hyphen ever reaches a VCL caption.

DAT.Runtime.FMX deliberately contains no hyphenation code at all. This is not an omission, and a test asserts it: the renderer knows the final width and the pack never can, so leaving the choice to the renderer is both simpler and better.

11. The layout planner

DAT.Review.Localization is the largest and most consequential unit in the product. It decides what has to change so that translated text fits, and it produces a list of proposals rather than applying anything itself.

11.1 The measurement seam

Text is measured with the engine that will actually draw it.

 

FrameworkUnitMeasures through
VCLDAT.Review.TextMeasurement.GDIGetTextExtentPoint32
FireMonkeyDAT.Review.TextMeasurement.FMXTTextLayout
EitherDAT.Review.TextMeasurementChooses from the catalog's framework

 

The seam is one function wide: a measurer answers a width for a run of text at a point size and weight, and nothing else. Everything the planner builds on top of that number is framework-neutral and shared. Measurers register themselves as they are linked in, so the planner never names a framework unit.

DPI is pinned to 96, because that is the basis a form was designed at.

The two engines do not agree, and the difference is large enough to matter: the same string at the same point size measures roughly a quarter narrower through TTextLayout than through GDI. That is precisely why the seam exists. Measuring with the wrong engine produces a plan that is confidently wrong in both directions at once, failing layouts that are fine and passing layouts that are not.

11.2 Framework facts encoded in the planner

Each of these produces wrong layouts if it is not known:

11.3 The phases

The planner runs a sequence of passes over a single resolved model, so that the values it finally exports agree with one another rather than describing conflicting placements.

  1. Phase 1 — start every control from its designer geometry.
  2. Phase 2 — size each control against its measured translated text. Rows of buttons, captions above fields, and evenly pitched rows are treated as sets rather than as individuals, because a row where one member grew and the others did not reads as a mistake.
  3. Phase 3 — resolve collisions repeatedly against the planned geometry, and hold captions inside the frames drawn around them.
  4. Phase 3d — the settling pass: level rows, widen and re-center headings, widen lone buttons rightward only, give stacked paragraphs one shared font size, and narrow wrapped text to the width its wrap actually uses.
  5. Phase 3e — mirror the form for a right-to-left language (section 12).
  6. Phase 4 — emit proposals from the settled geometry.

Two of those deserve their reasoning stated. A button is widened rightward only because a button is positioned against the thing it acts on, so it keeps the place it was drawn in. Wrapped text is narrowed rather than left at its designed width because a box wider than its wrap uses puts nearly all the words on the first line and one or two on the second, which is the ragged result a typesetter spends a career removing.

11.4 Trial and revert

Every step in the settling pass is applied speculatively and undone if it breaks something. A change that would push a control off its form, over its neighbor, or outside its container is reverted rather than kept. This is what allows the settling rules to be written independently of one another: a rule does not have to know what the other rules want, because a rule that fights another one loses and the geometry returns to what it was.

11.5 Geometry the application owns

DAT.Review.CodeGeometry reads the Pascal unit beside each form and notes any control whose Left, Top, Width, Height, Position.X, Position.Y, Align, BoundsRect, or SetBounds is assigned there. Those controls have their text translated and their geometry left entirely alone.

The reason is that an application which positions a control in code has already decided where it goes, and usually decides once, at startup. The planner reads the designer geometry, which is not the geometry the application will actually use, and proposes a position that overwrites a decision the application will never make again. Returning to the source language then restores the designed position rather than the computed one, so the control ends up somewhere it has never been.

The detection is deliberately literal: an assignment to a named identifier's geometry property, and nothing cleverer. No expression analysis, no following of variables, no with statements. A control it misses behaves as it did before; a control it claims wrongly loses only its layout adjustments and is still translated. Both directions fail softly, which is the right property for a heuristic that reads somebody else's source.

One reading serves both frameworks, because what is being read is Pascal rather than VCL or FireMonkey.

12. Right-to-left layout

A right-to-left interface is a reflected interface, not reversed text. Producing correctly translated words in a left-to-right arrangement is worse than refusing the language outright, because it looks as though it worked.

12.1 Choosing the VCL reading mode

The VCL offers three right-to-left modes, and they are not interchangeable.

 

BiDiModeFlips alignmentRTL readingLeft scroll bar
bdRightToLeftyesyesyes
bdRightToLeftNoAlignnoyesyes
bdRightToLeftReadingOnlynoyesno

 

Under bdRightToLeft the framework flips text alignment on its own. Since the planner already decides alignment for every control, that flip lands on top of the planner's decision and silently undoes it, producing a double flip that looks like a defect in the planner. bdRightToLeftNoAlign is therefore what the runtime uses: reading order and scroll bar side from the framework, alignment from the planner.

Digits need no help. A right-to-left run followed by a number renders with the letters reversed and the number intact, in both renderers, because the Unicode bidirectional algorithm handles it. Version strings, times, quantities, and paths are safe.

12.2 Mirroring in the planner, not in the framework

The VCL has BiDiMode and FlipChildren. FireMonkey has neither. Leaning on the VCL's mechanism would mean writing the FireMonkey half separately and getting different behavior on each, which principle 3.5 rules out. The mirror is therefore computed by the planner and emitted as the ordinary position rules the runtime already applies.

The transform is parent-relative, which handles nesting without recursion:

MirroredLeft := ParentInnerWidth - (PlannedLeft + PlannedWidth)

A form is never mirrored. A window has no parent to be reflected within, so the arithmetic degenerates into negating the window's own screen position, which moves the window off the side of the display. The planner explicitly skips the record whose component name is its own form name.

12.3 What mirrors, and what deliberately does not

 

MirrorsDoes not
Coordinates, within each parent
Align and TAlignLayout, for framework-placed controls
Anchors, where exactly one horizontal edge is anchored
Text alignment (center stays center)
Grid column order
Tab order
Reading order and scroll bar side (VCL)
Transport buttons — rewind, play, and stop refer to the direction a recording moves, not the direction a language is read, so the group moves to the mirrored side as a block and keeps its internal order

Numbers, times, versions, and paths — handled by the renderers

Images — the transform only ever moves a control, never its contents, so artwork and logos are safe by construction

 

A control anchored to both horizontal edges stretches, which is already symmetrical, and is left alone. A control anchored to neither has nothing horizontal to change. Only the one-edge case is mirrored, and getting this wrong is invisible until the user resizes the window.

12.4 Order of application

Reading order is applied before the text, and the reason is subtle enough to be worth documenting. Translating a menu item's caption causes the menu to be rebuilt, and Delphi stamps each item with the reading order in force at the moment of that rebuild. Applying direction after the text therefore rebuilds the menu in the direction being left behind, and then depends on the framework's own notification to correct it.

That notification is not dependable here. TMenu.DoBiDiModeChanged begins:

if (not SysLocale.MiddleEast) or (WindowHandle = 0) then Exit;

Both conditions bite. The window handle is momentarily zero while the form's window is recreated, which is exactly when BiDiMode changes. And on a machine whose Windows locale is not configured for those languages, the VCL does not lay menus out right-to-left at all, whatever BiDiMode says, so menu direction can behave differently on two machines running the same build. Both are worth knowing before trying to reproduce a menu problem.

Setting direction first removes the dependency entirely: whatever is rebuilt afterward is rebuilt the right way round to begin with.

13. The runtime pack

DAT.Core.RuntimePack writes the pack, which is the only artifact that ships. Schema 3 carries the language and locale, the translated strings by key, the source text for each key so that returning to the source language is possible, runtime templates for strings the application formats itself, font colors, and the layout rules.

13.1 What may be exported

Two rules govern what may go into a pack:

Both rules once had private copies elsewhere, and both silently deleted features. The pending case is the more instructive of the two, because it could not be fixed by fixing the code. RestoreDecisions reads the previous run's proposal file and copies each saved decision over the analyzer's, so that a rejection survives a re-scan. It also copied pending — and pending is not a decision, it is the absence of one. A proposal file written by a build that did not yet know a property existed recorded that property as pending, and every later run restored that over a freshly accepted decision. The feature was vetoed permanently by a stale file, and no amount of rebuilding the analyzer would have changed it. A saved pending now means "nobody has decided," and the analyzer's own judgment stands.

13.2 Why the source text ships too

The pack carries the source text for every key, not only the translation. Without it, selecting the source language again leaves the words in the last language chosen, however correctly the geometry is restored. This is easy to leave out and produces a fault that only appears on the second language change.

14. The runtime

DAT.Runtime.VCL and DAT.Runtime.FMX apply a pack to a live form. DAT.Components.Core holds the shared language manager behavior, and the two framework adapters differ only where the frameworks do.

14.1 Order of operations in ApplyToForm

  1. Snapshot the original geometry, once, before any language is applied.
  2. Restore from that snapshot.
  3. Apply reading order (section 12.4).
  4. Apply text, including collections such as grid headings.
  5. Apply layout rules in a fixed order: AutoSize, font size, WordWrap, text alignment, Align, size, position, Anchors, tab order, column widths, column order.
  6. Resolve soft hyphens, now that every control is its final size (VCL only).

The ordering is not arbitrary, and two steps in it are load-bearing. An auto-sizing label recomputes its own bounds from its text and discards an assigned width, so AutoSize must be cleared before anything else is set. Column widths name a column by its designed index, so the column order must be reversed last, or every width lands on the wrong column.

14.2 Snapshot and restore

Step 2 is the one that is easy to omit and hard to diagnose without. Applying a language must start from the form as it was designed, not from the form as the previous language left it. Otherwise each language inherits whatever the last one changed, and a rule that is simply absent from the new pack has nothing to undo it. The symptom is a form that drifts a little further wrong with every language change and is correct again after a restart.

The snapshot covers position, size, font size, AutoSize, WordWrap, text alignment, Align, Anchors, tab order, and grid column order — that is, exactly the set of properties the pack is allowed to change.

14.3 What is never restored

Color. The applicator does not set colors, so restoring one can only undo something the application itself did. An application that paints its own colors after a form is shown would have them overwritten with design-time values on every language change. The rule is general and worth stating as such: what we never changed, we never restore.

14.4 The language manager and form discovery

The component in the customer's application is a language manager: it loads packs from a folder, exposes the available languages, applies a chosen one, and remembers the choice. DAT.Runtime.Preference stores that choice per user.

Applying a language to the forms that are already open is the easy half. The harder half is a form created after the language was chosen — a dialog opened from a button — which must be translated when it is shown. The two frameworks solve this by entirely different mechanisms:

 

 VCLFireMonkey
Finding open formsScreen.FormsScreen.Forms and Screen.PopupForms
Noticing a new formHooks the window procedureSubscribes to TFormBeforeShownMessage through TMessageManager
Noticing a closed formWindow destructionTFormReleasedMessage

 

Because those mechanisms share nothing, one of them working says nothing at all about the other. Each has its own test (section 15.6).

15. Contracts: how this product is kept honest

This section describes the practice that most distinguishes this codebase. It is worth reading even by someone who intends to change nothing, because the rules here explain why the tests are shaped the way they are.

15.1 What a contract is

A layout contract is three files: a small purpose-built form, a catalog naming the translated text for it, and an expectation file stating in numbers what the planner must do with them. Assertions are numeric because layout is numeric — a right edge that must not move, a control that must keep its place, a caption that must still hold its text.

The forms are purpose-built rather than taken from a real application, so each rule is proved on the shape of a problem rather than on one project's particular arrangement of controls.

64 layout contracts currently run, alongside 3 form-scan fixtures, 9 pascal-scan checks, and 30 test harnesses.

15.2 A contract must fail first

The single most important rule. A contract whose expected values are copied from what the code currently does proves nothing; it records behavior instead of requiring it.

Two contracts in this project had to be rebuilt for exactly that fault. Both passed from the day they were written, and both were worthless: one flipped a constant on a fixture that never reached that code path, and the other used a caption that lacked the structure the rule depended on.

The discipline is therefore: write the assertion, watch it fail for the right reason, then make it pass. Where a test is written after the code, which does happen, it is verified by deliberately disabling the code and confirming that the test fails. A test that has never been seen to fail is an untested test.

15.3 Contracts must cross seams, not sit at the ends

 

 

The hardest defects this product has had all shared one shape: a value correct at both ends of the pipeline and absent in the middle.

Three separate failures removed the right-to-left feature before anyone noticed:

  1. One applicator held its own copy of the allowed-property list and dropped every mirroring rule.
  2. The pack exporter held another copy and dropped them again.
  3. The auto-accept rule held a third, so the proposals were created pending, and the saved-decision restore made that permanent.

Every one of those was invisible to every test that existed, because the tests sat at the ends. The layout contracts passed because the plan was right. The applicator tests passed because they were handed a pack written by hand. Nothing crossed the joins between them.

Three tests now do. PackLayoutSmokeTests goes proposal to pack, the contract harness can assert a proposal's decision as well as its value, and ProposalDecisionSmokeTests goes run to run. The first of those failed on seven of eight checks the day it was written, which is what a seam test is supposed to do.

15.4 A test must not disturb what it measures

A diagnostic added to the VCL runtime read TMenu.Handle in order to report the native right-to-left flag. TMenu.Handle creates and populates the menu if it does not already exist, and doing that in the middle of applying a language destroyed the translated menu captions. The runtime smoke test failed within seconds of the diagnostic being switched on, which is exactly what that test is for. The diagnostic was changed to ask Windows for the menu already attached to the window, which creates nothing.

15.5 Guards around the suite

15.6 Framework parity

The 64 contracts cover 33 distinct behaviors: 31 proven on both frameworks, 2 on the VCL only, and none on FireMonkey only. The two are VCL-only by design rather than by omission, and Appendix B says why for each.

Parity is not the same as identity, and this is the trap to understand before writing a twin. Because the planner measures through a seam, the same rule legitimately produces different numbers on each framework. A fixture built by copying its twin's font size reproduces that twin's numbers but not its situation: text that needs a font reduction under one engine may fit comfortably under the other, so the rule is never asked to do anything and the contract watches nothing. Twins must be reasoned from what the layout should be, and each must be seen to fail before the code is right.

The test harnesses pair the same way: seven matched pairs, one VCL-only harness covering MDI, which is a VCL concept with no FireMonkey equivalent, and the rest framework-neutral.

16. Extending the product

This section is procedural. It assumes the preceding sections have been read.

16.1 Adding a layout rule

Write the contract first. Create the three fixture files under contracts\layout, run the suite, and confirm that the new contract fails for the reason you expect. Then add the rule to the settling pass in DAT.Review.Localization, making it trial-and-revert like its neighbors. Run the whole suite, not only the new contract: a settling rule that fights an existing one shows up as a failure elsewhere, and that failure is information.

If the rule applies to both frameworks, write the twin at the same time, and read section 15.6 before choosing its numbers.

16.2 Adding a runtime layout property

This is the change most likely to be silently lost, because it crosses three boundaries. In order:

  1. Add the property to IsRuntimeLayoutProperty in DAT.Runtime.LanguagePack. This is the only list; do not add a second one anywhere.
  2. Emit a proposal for it in the planner, and make sure it is created accepted if it should apply by default.
  3. Apply it in both applicators, in the right place in the fixed order of section 14.1.
  4. Add it to the snapshot, or a later language change will not be able to undo it.
  5. Delete any saved proposal file from earlier runs while testing, or a stale pending entry will veto the new property and the feature will appear not to work (section 13.1).

Then extend PackLayoutSmokeTests, which is the test that crosses from proposal to pack and is the one that would have caught each of the historical failures in section 15.3.

16.3 Adding a translation provider

Implement the transport in a new unit beside DAT.Provider.Client and reuse the four units around it: placeholders, batching, language codes, and retry are not provider-specific and should not be reimplemented. The provider-specific work is the request format, the response format, the error body, and the list of language codes the service accepts. Extend DAT.Provider.LanguageCodes with that list rather than passing codes through, and read section 9.2 for why dropping a region is the safe direction.

16.4 Adding a language

A language needs an entry in the Wizard's list with its text direction, and, if it is a compounding language, a hyphenation dictionary under the shared store (section 10.1). Everything else follows from the catalog: the direction drives mirroring, and the locale block drives format settings.

If the language is right-to-left, expect to verify mirroring on a machine whose Windows locale supports those languages, for the reason given in section 12.4.

16.5 Adding a test harness

Harnesses live in tools\tests as console programs that compile directly against the product's source and exit non-zero on failure. Keep the framework-specific ones in pairs. Where the two frameworks genuinely behave differently, assert the difference rather than papering over it: a test that asserted identical soft-hyphen behavior on both frameworks (section 10.3) would be asserting that one of the two runtimes has a defect.

17. Build and release validation

All four configurations — Win32 and Win64, Debug and Release — are rebuilt after every change. The release validation script requires one uninterrupted pass of the complete matrix, plus the Studio launch and self-localization smoke tests.

The full suite comprises the 64 layout contracts, 3 form-scan fixtures, 9 pascal-scan checks, and the 30 harnesses covering the foundation, scanner, catalog, runtime, validation, and export paths; the VCL and FireMonkey runtime smoke tests; the four language-manager suites; and the focused harnesses for retry, language codes, context batching, proposal decisions, pack export, placeholders, hyphenation, context, wrap, discovery, and right-to-left on both frameworks.

Runtime packages must be rebuilt separately when a runtime unit changes. An application that links them will not otherwise pick up the change, and the symptom is a fix that appears not to work.

18. Known limits

Recorded in full in docs\guides\Fix List.md. The ones an engineer should know before planning work:

Appendix A — Unit inventory

 

UnitPurpose
core
DAT.Core.TypesCatalog, entry, locale, and enumeration types
DAT.Core.CatalogJsonCatalog read and write
DAT.Core.RuntimePackBuilds the shipped pack
DAT.Core.GlossaryPer-project approved terms
DAT.Core.SharedDictionaryPer-language wording shared across applications
DAT.Core.HyphenationPer-language break dictionaries
DAT.Core.TerminologyAuthoritative term resolution
DAT.Core.TranslationWorkspaceWhere every artifact lives
DAT.Core.ProjectDetectionRecognizing a Delphi project and its framework
DAT.Core.AITranslationThe copy-and-paste AI workflow
scan
DAT.Scan.ProjectDrives the scan
DAT.Scan.FormTextReads .dfm and .fmx
DAT.Scan.PascalResourcesReads .pas
DAT.Scan.ContextWrites each string's context sentence
DAT.Scan.DomainProfileVocabulary and word-sense resolution
DAT.Scan.RulesText ownership classification
DAT.Scan.QualityQuality checks on scanned text
DAT.Scan.TextCodecSource file encoding
DAT.Scan.CatalogMergeMerging a re-scan into an edited catalog
DAT.Scan.TypesScan-layer types
review
DAT.Review.LocalizationThe layout planner
DAT.Review.TextMeasurementThe measurement seam
DAT.Review.TextMeasurement.GDIVCL measurement
DAT.Review.TextMeasurement.FMXFireMonkey measurement
DAT.Review.CodeGeometryControls the application positions itself
provider
DAT.Provider.ClientService transport
DAT.Provider.PlaceholdersFormat specifier protection
DAT.Provider.BatchingGrouping by shared context
DAT.Provider.LanguageCodesCodes a service will accept
DAT.Provider.RetryRate limits and backoff
DAT.Provider.CredentialStoreAPI keys
DAT.Provider.SettingsProvider settings
DAT.Provider.TypesProvider-layer types
runtime (ships in the customer's application)
DAT.Runtime.LanguagePackPack loading; the one allowed-property list
DAT.Runtime.VCLApplying a pack to a VCL form
DAT.Runtime.FMXApplying a pack to a FireMonkey form
DAT.Runtime.ManagerLanguage selection and format settings
DAT.Runtime.PreferenceRemembering the chosen language
components (ship in the customer's application)
DAT.Components.CoreShared language-manager behavior
DAT.Components.VCLVCL adapter
DAT.Components.FMXFireMonkey adapter
DAT.Components.VCL.LanguageSelectorOptional bound selector, VCL
DAT.Components.FMX.LanguageSelectorOptional bound selector, FireMonkey
validation, integration, studio
DAT.Validation.CatalogThe export gate
DAT.Integration.*Component kits, packages, source integration, deployment
DAT.Studio.*Main form, Setup Wizard, translation, localization review

 

Appendix B — Contract inventory and framework parity

Contract names are file identifiers and are reproduced exactly as they appear on disk.

B.1 Proven on both frameworks — 31 behaviors

a_button_gets_room_for_its_caption; a_caption_stops_at_the_button_beside_it; a_heading_widens_before_it_wraps; a_long_word_gets_room; a_paragraph_stays_on_the_form; button_above_grid_padding; button_keeps_its_place_when_text_grows; button_row_keeps_even_pitch; caption_above_field_takes_its_column; caption_far_wider_than_its_box; centred_heading_widens_about_centre; checkbox_caption_inside_container; checkbox_caption_note; code_positioned_control_is_left_alone; container_keeps_its_size; designed_overlap_is_left_alone; email_label_button_padding; frame_grows_and_says_so; grid_headers_fit; intro_paragraph_wraps_compact; left_caption_takes_left_margin; long_button_and_field_stack; media_button_row_container; memo_label_pair; preserve_label_font; right_aligned_caption_grows_leftward; right_to_left_flips_alignment_and_anchors; right_to_left_mirrors_the_form; stacked_paragraphs_share_a_size; transport_buttons_keep_their_order; wrapped_text_is_balanced.

B.2 VCL only, by design — 2 behaviors

 

ContractWhy there is no twin
inherited_font_is_the_forms_fontFont inheritance down the object tree was changed for the VCL alone, on VCL evidence. FireMonkey inherits through StyledSettings and TextSettings instead, so its equivalent is a different contract rather than a twin.
a_grid_does_not_end_the_formGuards a .dfm parsing hazard specifically: a collection written as item … end blocks, whose end lines emptied the object stack. FireMonkey files have no such syntax.

 

B.3 Harness parity

Thirty harnesses: seven matched framework pairs — design streaming, language manager, manager lifecycle, right-to-left, runtime smoke, discovery, and wrap — one VCL-only harness covering MDI, which has no FireMonkey equivalent, and the remainder framework-neutral, covering scanning, providers, packs, hyphenation, context, and the contract runners themselves.

Appendix C — Files, folders, and where things live

 

WhatWhere
Development catalog%LOCALAPPDATA%\DelphiAppTranslationStudio\Workspaces\<project>\Development
Runtime packs…\Workspaces\<project>\Languages, deployed to Localization\Languages beside the executable
Layout proposal and reviewexport\localization-review\<project>\<language>
Shared dictionaries, terms, hyphenationC:\Users\Public\Documents\Delphi App Translation
Language preference (target application)%LOCALAPPDATA%
Layout contractscontracts\layout
Test harnessestools\tests
Engineering notes and Fix Listdocs\guides

 

This guide describes the product as it stood on August 21, 2026. Where it disagrees with the code, the code is right and this document is stale. The Engineering Notes in docs\guides\Engineering Notes.md carry the running record of change.